「這個物件都能正常 new 出來、程式也沒噴任何錯誤,代表這個依賴注入設定是對的吧?」
答案是:不一定。今天要講的這個陷阱,最麻煩的地方就在這裡——它不會在物件建立的當下爆炸,只會在你真正需要它做點什麼的那一刻,才冷不防炸開。而「建立成功、沒有報錯」正是 AI 判斷「這個依賴注入設定沒問題」時最容易誤信的假訊號。
昨天講的是同一個容器單例,因為在建構子快取了不該快取的物件而出包;今天要講的是同一類容器機制的另一個陷阱——這次問題不在「快取了什麼」,而在「型別提示提示得太具體」。
假設有一段程式碼,建構子長這樣:
❌ 型別提示具體類別,指望 autowire 解析出「正確配置好的」實例:
class ReportGenerator
{
public function __construct(private AppContainer $container) {}
public function generate(): array
{
$service = $this->container->get(SomeInterface::class);
return $service->buildReport();
}
}
這段程式碼在絕大多數情況下看起來完全正常:ReportGenerator 被建立出來,沒有任何錯誤訊息,型別提示也對,語法檢查全過。問題出在 AppContainer 是一個自訂的具體類別,不是介面——PHP-DI 這類自動裝配容器,遇到型別提示是具體類別、而這個類別又沒有被明確綁定過的情況,會嘗試用 autowire 機制「猜」一個實例出來:直接呼叫這個類別的建構子生一個新物件。
聽起來合理,但實際發生的事情是:容器真的生出了一個 AppContainer 的新實例——只是這個新實例完全沒有跑過應用程式原本的 bootstrap 流程,裡面沒有任何綁定設定。它是一個看起來型別正確、實際上是空殼的容器。
這正是這個陷阱最陰險的地方:new ReportGenerator() 這一步完全不會出錯。容器 autowire 出一個空殼 AppContainer,塞進 ReportGenerator 的建構子,一切順利完成。物件建立起來了,型別對了,沒有任何警告。
真正爆炸的時刻,是後面呼叫 generate()、$this->container->get(SomeInterface::class) 執行的那一刻——因為這個空殼容器裡沒有 SomeInterface 的任何綁定設定,會直接丟出「the class is not instantiable」這類錯誤。
如果你請 AI 驗證這段程式碼有沒有問題,AI 很可能只做到「物件能不能建立成功」這一層測試,因為建立過程真的沒有任何異常訊號。**這正是 Day 01 那句話在依賴注入層的具體樣貌:AI 給出「已確認能建立、沒有錯誤」的結論,範圍其實只涵蓋到物件建立那一刻,沒有涵蓋到這個物件真正被使用時的行為。**兩者聽起來很接近,實際上是完全不同層級的驗證。
✅ 不依賴 autowire 猜測容器實例,改用明確的全域存取點:
class ReportGenerator
{
public function generate(): array
{
$service = app(SomeInterface::class);
return $service->buildReport();
}
}
這裡的關鍵改變不是「換一種語法比較潮」,而是不再讓容器對著一個具體類別做自動裝配的猜測。全域的 app() helper(或任何等效的明確存取點)直接拿到的是應用程式啟動時真正 bootstrap 過的那個容器實例,不會有「猜出一個空殼」的問題。
如果專案架構上真的需要把容器當依賴傳進來,正確做法是型別提示對應的介面(例如 ContainerInterface),並且在應用程式啟動時明確把介面綁定到正確配置好的實例——這樣容器解析到的物件,一定是那個綁定過的實例,不會有 autowire 憑空生一個新物件的空間。
把這兩個 DI 容器陷阱(昨天的單例快取、今天的 autowire 空殼)放在一起看,會發現一個共通的判斷原則:型別提示提示得越具體,容器能自己「猜」出東西來補上的空間就越大,而猜出來的東西是不是你要的那個,往往要等到真正用到的那一刻才知道。 型別提示一個沒有明確綁定的具體類別,容器唯一能做的事就是憑建構子簽章生一個新物件出來——這個新物件不會是應用程式裡「那個」正確配置好的實例,只會是同名同姓的陌生人。
AI 在檢視依賴注入設定時,容易只確認「型別對不對」,卻不會主動去確認「這個型別背後,容器到底是解析到綁定好的實例,還是自己猜生了一個新的」——這兩件事在語法層面完全看不出差別,只有在執行期才會露餡。
這裡今天講的具體行為(PHP-DI 對未綁定具體類別做 autowire 的方式)是這個專案用的容器實作特有的細節,不是所有 DI 容器都一樣,不同容器對這種情況的處理甚至完全相反。Spring 對沒有明確 @Bean 設定的具體類別做元件掃描時,也可能出現類似「猜測空間隨型別提示越具體而放大」的風險模式;但 .NET Core 內建 DI 容器反而走了另一條路——遇到沒註冊過的具體型別時,會立刻丟出明確的例外,直接告訴你「這個型別沒有被註冊」,不會靜默生出空殼物件、更不會延遲到後續呼叫才爆炸。這其實是個值得對照的設計取捨:PHP-DI 選擇靜默 autowire 來降低設定成本,.NET Core 選擇立即報錯來換取更高的可預期性,兩種設計沒有絕對的對錯,但對「型別提示越具體,容器行為就越難預測」這件事,各家容器給出的答案並不一樣。真正該記住的判斷原則是:依賴注入的型別提示越靠近介面,行為就越可預期;越靠近具體類別,就越依賴容器當下的自動裝配規則——而這條規則會不會在你意識到之前就先爆炸,取決於你用的容器選擇了哪一種設計。
回想你手上專案裡的建構子型別提示:有沒有哪個地方型別提示的是一個具體類別,而不是介面?你能確定容器在那個地方一定解析到「正確配置好的那個實例」,而不是自動裝配猜出來的空殼嗎?
明天要離開依賴注入的話題,回到資料本身——數值精度為什麼是金額運算裡不能妥協的一件事,以及為什麼這套系統強制要求用 bcmath,而不是相信原生的浮點數運算。